﻿/*
 *
 * The contents of this file are subject to the Mozilla Public License Version 1.0 (the "License"); 
 * you may not use this file except in compliance with the License.
 * 
 * You may obtain a copy of the License at http://www.mozilla.org/MPL/
 * 
 * Software distributed under the License is distributed on an "AS IS" basis, 
 * WITHOUT WARRANTY OF ANY KIND, either express or implied.
 * 
 * See the License for the specific language governing rights and limitations under the License.
*/

using System;
using System.Text;
using System.Security.Cryptography;
using System.Web;
using System.Collections;
using System.Collections.Generic;

namespace FBConnectAuth
{
	public class FBConnectAuthentication
	{
		private readonly string appSecret;
		private readonly string apiKey;

		/// <summary>
		/// Initiliazes an instance of <see cref="FBConnectAuthentication"/>.
		/// </summary>
		/// <param name="apiKey">The apiKey of your Facebook application</param>
		/// <param name="appSecret">Your Facebook application's "secret"</param>
		public FBConnectAuthentication(string apiKey, string appSecret)
		{
			if (String.IsNullOrEmpty(apiKey))
			{
				throw new ArgumentException("apiKey cannot be null or empty","apiKey");
			}
			if (String.IsNullOrEmpty(appSecret))
			{
				throw new ArgumentException("appSecret cannot be null or empty", "appSecret");
			}

			this.appSecret = appSecret;
			this.apiKey = apiKey.ToLowerInvariant();
		}
		
		/// <summary>
		/// Validates that the request came from a validated Facebook user using the cookies from the current <see cref="HttpRequest"/>.
		/// </summary>
		/// <returns>A <see cref="ValidationState"/> value, indicating whether the request was valid.</returns>
		/// <exception cref="InvalidOperationException">Thrown when HttpContext.Current is null</exception>
		public ValidationState Validate()
		{
			if (HttpContext.Current == null)
			{
				throw new InvalidOperationException("This method can only be used from within an ASP.NET web request.");
			}
			return Validate(HttpContext.Current.Request.Cookies);
		}

		/// <summary>
		/// Validates that the request came from a validated Facebook user.
		/// </summary>
		/// <param name="cookies">A collection of cookies containing the Facebook Connect auth cookies.</param>
		/// <returns>A <see cref="ValidationState"/> value, indicating whether the request was valid.</returns>
		/// <exception cref="ArgumentNullException">Thrown when the <paramref name="cookies"/> parameter is null.</exception>
		public ValidationState Validate(HttpCookieCollection cookies)
		{
			if (cookies == null)
			{
				throw new ArgumentNullException("cookies");
			}

			// See http://wiki.developers.facebook.com/index.php/Verifying_The_Signature for more info
			string signature = GetSignatureFromCookies(cookies);
			
			if (signature == null)
			{
				return ValidationState.NoSignatureFound;
			}

			SortedList<string, string> sortedCookieValues = ExtractAndSortFBCookies(cookies);
			
			var sb = new StringBuilder();
			foreach (KeyValuePair<string, string> pair in sortedCookieValues)
			{
				
				sb.AppendFormat("{0}={1}", pair.Key, pair.Value);
			}
			
			sb.Append(appSecret);
			string stringToHash = sb.ToString();

			StringBuilder computedHash = new StringBuilder();
			byte[] hash = MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(stringToHash));
			foreach (byte b in hash)
			{
				computedHash.AppendFormat("{0:x2}", b);
			}

			bool isSigValid = computedHash.ToString().ToLowerInvariant() == signature.ToLowerInvariant();
			return isSigValid ? ValidationState.Valid : ValidationState.InvalidSignature;
		}

		private SortedList<string, string> ExtractAndSortFBCookies(HttpCookieCollection cookies)
		{
			var result = new SortedList<string, string>();
			string cookiePrefix = apiKey + "_";
			foreach (string cookieName in cookies)
			{
				if (cookieName.StartsWith(cookiePrefix))
				{
					var cookie = cookies[cookieName];
					result.Add(cookie.Name.Substring(cookiePrefix.Length),cookie.Value);
				}
			}
			return result;
		}

		

		private string GetSignatureFromCookies(HttpCookieCollection cookies)
		{
			var sigCookie = cookies[apiKey];
			return sigCookie != null ? sigCookie.Value : null;
		}

		/// <summary>
		/// Gets a FB Connect session from the values in the cookies from the current <see cref="HttpRequest"/>.
		/// </summary>
		/// <returns>A FB Connect Session object</returns>
		/// <exception cref="ArgumentNullException">Thrown when <paramref name="cookies"/> is null.</exception>
		/// <exception cref="FBConnectAuthenticationException">Thrown when the signature cannot be validated.</exception>
		/// <exception cref="ArgumentOutOfRangeException">Thrown when the expires cookie value is not a valid integer.</exception>
		/// <exception cref="InvalidOperationException">Thrown when HttpContext.Current is null</exception>
		public FBConnectSession GetSession()
		{
			if (HttpContext.Current == null)
			{
				throw new InvalidOperationException("This method can only be used from within an ASP.NET web request.");
			}
			return GetSession(HttpContext.Current.Request.Cookies);
		}

		/// <summary>
		/// Gets a FB Connect session from the values in the <paramref name="cookies"/>.
		/// </summary>
		/// <param name="cookies">Cookies containing FB Connect session information</param>
		/// <returns>A FB Connect Session object</returns>
		/// <exception cref="ArgumentNullException">Thrown when <paramref name="cookies"/> is null.</exception>
		/// <exception cref="FBConnectAuthenticationException">Thrown when the signature cannot be validated.</exception>
		/// <exception cref="ArgumentOutOfRangeException">Thrown when the expires cookie value is not a valid integer.</exception>
		public FBConnectSession GetSession(HttpCookieCollection cookies)
		{
			if (cookies == null)
			{
				throw new ArgumentNullException("cookies");
			}

			ValidationState validationState = Validate(cookies);
			if (validationState != ValidationState.Valid)
			{
				throw new FBConnectAuthenticationException("Cannot create FB Connect session.",validationState);
			}

			return CreateFBSessionFromCookies(cookies);
		}

		private FBConnectSession CreateFBSessionFromCookies(HttpCookieCollection cookies)
		{
			string cookiePrefix = apiKey + "_";
			int expires;
			if (!Int32.TryParse(cookies[cookiePrefix + "expires"].Value,out expires))
			{
				throw new ArgumentOutOfRangeException("cookies", "The value of 'expires' cookies is not a valid integer.");
			}
			string userId = cookies[cookiePrefix + "user"].Value;
			string sessionKey = cookies[cookiePrefix + "session_key"].Value;
			string sessionSecret = cookies[cookiePrefix + "ss"].Value;
			return new FBConnectSession(expires,sessionKey,sessionSecret,userId);
		}
	}
}
